package com.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.common.constant.ProductConstant;
import com.common.to.es.SkuEsModel;
import com.common.utils.R;
import com.search.config.ElasticSearchConfig;
import com.search.constant.EsConstant;
import com.search.feign.ProductFeignService;
import com.search.service.SearchService;
import com.search.to.AttrTo;
import com.search.to.BrandTo;
import com.search.to.CatalogTo;
import com.search.vo.SearchParam;
import com.search.vo.SearchResult;
import com.sun.org.apache.xml.internal.resolver.CatalogEntry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.ParsedAggregation;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.swing.text.Highlighter;
import javax.swing.text.StyledEditorKit;
import javax.xml.transform.Source;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Author Rookie-6688
 * @Description
 * @Create 2021-04-02 16:09
 */
@Slf4j
@Service
public class SearchServiceImpl implements SearchService {

    @Resource
    RestHighLevelClient restHighLevelClient;
    @Resource
    ProductFeignService productFeignService;

    /**
     * 从 ES 中检索数据
     * @param searchParam
     * @return
     */
    @Override
    public SearchResult search(SearchParam searchParam) {
        // 1、准备检索请求
        SearchRequest searchRequest = buildSearchRequest(searchParam);
        SearchResult searchResult = null;
        // 2、执行检索请求
        try {
            SearchResponse searchResponse = restHighLevelClient.search(searchRequest, ElasticSearchConfig.COMMON_OPTIONS);
            // 3、分析响应数据封装成我们需要的格式
            searchResult = buildSearchResult(searchResponse,searchParam);
            log.info("------返回首页数据\n"+searchResult);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return searchResult;
    }

    /**
     * 获取检索请求对象
     * @param searchParam
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam searchParam) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        /**
         * 1、查询
         */
        // 1）构建bool批查询对象
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        // 2）must 模糊查询（sku标题匹配）
        if(StringUtils.isNotBlank(searchParam.getKeyword())){
            boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",searchParam.getKeyword()));
        }
        // 3）filter 查询
        // 分类
        if(searchParam.getCatalog3Id() != null){
            boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",searchParam.getCatalog3Id()));
        }

        // 是否有货
        // 传来的是数字，0无货，1有货
        if(searchParam.getHasStock() != null){
            boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",searchParam.getHasStock() == 1));
        }

        // 品牌ID，多选数组
        if(searchParam.getBrandId() != null && searchParam.getBrandId().size()>0){
            boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",searchParam.getBrandId()));
        }

        // 属性
        // 可能是多个属性匹配，所以分为数组，并且属性值可能是多选，所以结构是
        // attrs=1_5寸:8寸&attrs=2_16G:8G
        // 数组间使用&隔开，属性值使用:隔开，属性id和属性值使用_隔开
        if(searchParam.getAttrs() != null && searchParam.getAttrs().size()>0){
            for (String attr : searchParam.getAttrs()) {
                // attrs=1_5寸:8寸
                BoolQueryBuilder nestBoolQueryBuilder = QueryBuilders.boolQuery();  // 先创建一个查询对象，然后以此来创建nest查询对象
                String[] attrArray = attr.split("_");
                // 属性ID
                Long attrId = Long.valueOf(attrArray[0]);
                nestBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId",attrId));
                // 属性值
                String[] attrValueArray = attrArray[1].split(":");
                nestBoolQueryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue",attrValueArray));

                // 以查询条件来创建nest查询对象，第三个参数是参与评分规则，ScoreMode.None表示不参与评分
                NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", nestBoolQueryBuilder, ScoreMode.None);
                // 因为每个属性和选中的属性值都是一个条件，所以应该在循环时filter多个query对象，每个query对应一个属性ID和选中的属性值
                boolQueryBuilder.filter(nestedQueryBuilder);
            }
        }

        // 价格区间
        // 传来的是字符串，格式是
        // 1、1_500（大于等于(gte)1，小于等于(lte)500）
        // 2、_500（小于等于500）
        // 3、500_（大于等于500）
        String skuPrice = searchParam.getSkuPrice();
        if(StringUtils.isNotBlank(skuPrice)){
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
            if(skuPrice.startsWith("_")){
                // 第二种情况
                rangeQueryBuilder.lte(skuPrice.substring(1));
            }else if(skuPrice.endsWith("-")){
                // 第三种情况
                rangeQueryBuilder.gte(skuPrice.substring(0,skuPrice.length()-1));
            }else{
                // 第一种情况
                String[] ss = skuPrice.split("_");
                rangeQueryBuilder.gte(ss[0]);
                rangeQueryBuilder.lte(ss[1]);
            }
            boolQueryBuilder.filter(rangeQueryBuilder);
        }
        // 将批查询条件加入 searchQueryBuilder 对象中
        searchSourceBuilder.query(boolQueryBuilder);


        /**
         * 2、排序，分页，高亮
         */
        // 2.1 排序
        // 排序结构是：sort=hotScore_asc
        if(StringUtils.isNotBlank(searchParam.getSort())){
            String[] sortArray = searchParam.getSort().split("_");
            String sortName = sortArray[0];
            SortOrder sortOrder = sortArray[1].equalsIgnoreCase("asc")? SortOrder.ASC:SortOrder.DESC;
            searchSourceBuilder.sort(sortName,sortOrder);
        }

        // 2.2 分页
        Integer pageNum = searchParam.getPageNum();
        // 起始索引 = (当前页-1)*单页显示数据量
        searchSourceBuilder.from((pageNum-1)* EsConstant.PRODUCT_PAGE_SIZE);
        searchSourceBuilder.size(EsConstant.PRODUCT_PAGE_SIZE);

        // 2.3 高亮
        if(StringUtils.isNotBlank(searchParam.getKeyword())){
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");     // 高亮字段
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");
            searchSourceBuilder.highlighter(highlightBuilder);
        }

        /**
         * 3、聚合
         */
        // 3.1 品牌聚合
        TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg").field("brandId").size(10);
        // 子聚合
        brandAgg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
        brandAgg.subAggregation(AggregationBuilders.terms("brand_image_agg").field("brandImg").size(1));
        searchSourceBuilder.aggregation(brandAgg);

        // 3.2 分类聚合
        TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(10);
        catalogAgg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
        searchSourceBuilder.aggregation(catalogAgg);

        // 3.3 属性聚合
        // 定义
        NestedAggregationBuilder attrNestAgg = AggregationBuilders.nested("attrNestAgg", "attrs");
        // 一级
        TermsAggregationBuilder attrId_agg = AggregationBuilders.terms("attrId_agg").field("attrs.attrId").size(1);
        attrNestAgg.subAggregation(attrId_agg);
        // 二级
        attrId_agg.subAggregation(AggregationBuilders.terms("attrValue_agg").field("attrs.attrValue").size(50));
        attrId_agg.subAggregation(AggregationBuilders.terms("attrName_agg").field("attrs.attrName").size(1));
        searchSourceBuilder.aggregation(attrNestAgg);

        log.info("ES检索语句：---------------\n"+ searchSourceBuilder.toString());

        SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
        return searchRequest;
    }

    /**
     * 将得到的对象对象进行解析，返回前端需要的数据格式
     * @param searchResponse
     * @return
     */
    private SearchResult buildSearchResult(SearchResponse searchResponse,SearchParam searchParam) {
        SearchResult searchResult = new SearchResult();

        /**
         * ES检索结果解析
         */
        // 1、设置商品属性（查询结果）
        // 获取总查询结果对象
        SearchHits hits = searchResponse.getHits();
        if(hits.getHits() != null){
            List<SkuEsModel> skuEsModelList = Arrays.asList(hits.getHits()).stream()
                    .map(hit->{
                        SkuEsModel skuEsModel = JSON.parseObject(hit.getSourceAsString(), SkuEsModel.class);
                        if(StringUtils.isNotBlank(searchParam.getKeyword())){
                            // 判断是否输入检索字段，如果输入了将高亮后的数据设置到属性中去
                            HighlightField highlightField = hit.getHighlightFields().get("skuTitle");     // 高亮字段对象
                            String newTitle = highlightField.getFragments()[0].toString(); // 获取高亮字符串
                            skuEsModel.setSkuTitle(newTitle);
                        }
                        return skuEsModel;
                    }).collect(Collectors.toList());
            searchResult.setProduct(skuEsModelList);
        }

        // 2、设置分页相关属性
        // 设置总记录数
        long total = hits.getTotalHits().value;
        searchResult.setTotal(total);
        // 设置页码
        searchResult.setPageNum(searchParam.getPageNum());
        // 设置总页码
        int totalPages = (int) (total % EsConstant.PRODUCT_PAGE_SIZE == 0 ? (total / EsConstant.PRODUCT_PAGE_SIZE) : (total / EsConstant.PRODUCT_PAGE_SIZE + 1));
        searchResult.setTotalPages(totalPages);
        // 设置页码集合
        ArrayList<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i < totalPages; i++) {
            pageNavs.add(i);
        }
        searchResult.setPageNavs(pageNavs);

        /**
         * ES 聚合解析（聚合类型通过debug查询）
         */
        // 总的聚合对象（下面的各个信息都是聚合）
        Aggregations aggregations = searchResponse.getAggregations();
        // 3、设置分类信息
        ParsedLongTerms catalog_agg = aggregations.get("catalog_agg");
        // 在聚合对象的 buckets 属性里存储着聚合的值，来获取聚合结果数组
        List<? extends Terms.Bucket> catalogBuckets = catalog_agg.getBuckets();
        ArrayList<SearchResult.CatalogVo> catalogVoArrayList = new ArrayList<>();
        // 循环聚合结果
        for (Terms.Bucket bucket : catalogBuckets) {
            // 获取当前聚合的子聚合catalog_name_agg 聚合对象
            ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
            // 获取catalog_name_agg 聚合对象存储的name值，因为一个 catalogId 只对应一个 catalogName，所以这里直接取第一个元素get(0)
            String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString();
            // 通过catalog_agg聚合的key属性存储的 catalogId 以及 catalog_name_agg聚合的key存储的 name 创建catalogVo对象
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo((Long) bucket.getKeyAsNumber(), catalogName);
            // 添加到数组
            catalogVoArrayList.add(catalogVo);
        }
        // 将分类数据添加到对象属性中
        searchResult.setCatalogs(catalogVoArrayList);

        // 4、设置品牌信息
        ParsedLongTerms brand_agg = aggregations.get("brand_agg");
        List<? extends Terms.Bucket> brandAggBuckets = brand_agg.getBuckets();
        ArrayList<SearchResult.BrandVo> brandVoArrayList = new ArrayList<>();
        for (Terms.Bucket brandAggBucket : brandAggBuckets) {
            // 获取品牌名，在品牌聚合的子聚合中，和上面分类一样，id只对应一个名字，所以直接获取第一个元素
            ParsedStringTerms brand_name_agg = brandAggBucket.getAggregations().get("brand_name_agg");
            String branName = brand_name_agg.getBuckets().get(0).getKeyAsString();
            // 获取品牌logo图片，在品牌聚合的子聚合中，和上面品牌一样，id只对应一个图片，所以直接获取第一个元素
            ParsedStringTerms brand_image_agg = brandAggBucket.getAggregations().get("brand_image_agg");
            String brandImage = brand_image_agg.getBuckets().get(0).getKeyAsString();
            // 创建对象
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandAggBucket.getKeyAsNumber().longValue(),branName,brandImage);
            // 添加到数组中
            brandVoArrayList.add(brandVo);
        }
        // 设置属性
        searchResult.setBrands(brandVoArrayList);

        // 5、设置属性
        ParsedNested attrNestAgg = aggregations.get("attrNestAgg");
        ParsedLongTerms attrId_agg = attrNestAgg.getAggregations().get("attrId_agg");
        ArrayList<SearchResult.AttrVo> attrVoArrayList = new ArrayList<>();
        for (Terms.Bucket bucket : attrId_agg.getBuckets()) {
            long attrId = bucket.getKeyAsNumber().longValue();
            // 获取子聚合 attrName，因为一个ID只对应一个Name所以直接取第一个
            ParsedStringTerms attrName_agg = bucket.getAggregations().get("attrName_agg");
            String attrName = attrName_agg.getBuckets().get(0).getKeyAsString();
            // 获取子聚合 attValue，包含多个值，所以需要遍历，封装成数组
            ParsedStringTerms attrValue_agg = bucket.getAggregations().get("attrValue_agg");
            List<String> attrValueList = attrValue_agg.getBuckets().stream().map(bucket1 -> bucket1.getKeyAsString()).collect(Collectors.toList());
            // 创建成一个Attr对象
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId,attrName,attrValueList);
            // 添加到数组中
            attrVoArrayList.add(attrVo);
        }
        // 设置到属性中
        searchResult.setAttrs(attrVoArrayList);


        // 6、设置面包屑导航数据
        List<SearchResult.NavVo> navVoList = new ArrayList<>();
        // 6.1 设置种类
        if(searchParam.getCatalog3Id() != null){
            R r = productFeignService.catalogInfo(searchParam.getCatalog3Id());
            if(r.getCode() != 0){
                log.error("设置面包屑查询分类对象失败！attrId:" + searchParam.getCatalog3Id());
            }else{
                CatalogTo catalogTo = r.getData(new TypeReference<CatalogTo>(){});
                String catalogName = catalogTo.getName();
                navVoList.add(new SearchResult.NavVo("分类",catalogName,null));
            }
        }

        // 6.2 设置属性
        if(searchParam.getAttrs() != null){
            // 当条件为空就不设置
            for (String attr : searchParam.getAttrs()) {
                // attrs=1_5寸:8寸&attrs=2_16G:8G
                String[] attrArray = attr.split("_");
                Long attrId = Long.valueOf(attrArray[0]);
                R r = productFeignService.info(attrId);
                if(r.getCode() != 0){
                    // 说明失败，跳过
                    log.error("设置面包屑查询属性对象失败！attrId:" + attrId);
                    continue;
                }
                // 成功则继续执行，创建对象并添加到数组中
                // 从返回的数据中获取到属性名
                AttrTo attrTo = r.getData("attr", new TypeReference<AttrTo>() {});
                String attrName = attrTo.getAttrName();
                // 从请求参数中拼接得到删除后的URL
                // 先对属性参数进行转码，防止乱码
                String url = transferEncodeURL(searchParam, attr,"attrs");

                // 添加已检索得属性项ID
                searchResult.getAttrIds().add(attrId);
                // 最后封装成对象，再添加到数组中
                navVoList.add(new SearchResult.NavVo(attrName,attrArray[1],url));
            }
        }

        // 6.3 设置品牌
        if(searchParam.getBrandId() != null){
            // 当条件为空就不设置
            R r1 = productFeignService.infoList(searchParam.getBrandId());
            if(r1.getCode() != 0){
                // 说明失败，跳过
                log.error("设置面包屑查询品牌集合失败！brandIdList:" + searchParam.getBrandId());
            }else{
                List<BrandTo> brandToList = r1.getData(new TypeReference<List<BrandTo>>(){});
                StringBuffer stringBuffer = new StringBuffer();
                brandToList.forEach(brandTo -> {
                    stringBuffer.append(brandTo.getName()+";");
                    String brandStringQuery = stringBuffer.toString();
                    // 创建对象面包屑对象
                    String brandIdURL = transferEncodeURL(searchParam, brandTo.getBrandId()+"", "brandId");
                    // 添加到数组中
                    log.info("-----------"+brandIdURL+"----------"+brandStringQuery);
                    navVoList.add(new SearchResult.NavVo("品牌",brandStringQuery,brandIdURL));
                });
            }
        }
        // 最后设置到返回对象的属性中
        searchResult.setNavs(navVoList);


        return searchResult;
    }

    /**
     *
     * @param searchParam 请求参数
     * @param value 当前参数值
     * @param name 当前参数名
     * @return
     */
    private String transferEncodeURL(SearchParam searchParam, String value,String name) {
        String encode = null;
        try {
            encode = URLEncoder.encode(value, "UTF-8");
            // 这里对空格的处理和前端发送请求是不一样的，如果字符串包含空格，传到后台会变成"+"，而前端解析是将%20转成空格，所以这里需要将"+"转成"%20"
            encode = encode.replace("+","%20");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String replaceRequest = null;
        if(searchParam.get_queryString().startsWith(name + "=")){
            // 如果是当前条件就是第一个，那么前面是没有 & 的
            replaceRequest = searchParam.get_queryString().replace(name+"=" + encode, "");
        }else{
            replaceRequest = searchParam.get_queryString().replace("&"+name+"=" + encode, "");
        }
        String url = "http://search.gulimall.com/list.html?" + replaceRequest;
        return url;
    }

}
